﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom.Compiler;
using System.Reflection;

namespace SimplePad.Plugins
{
	public class PluginManager<ClientInterface, HostInterface>
	{
		public PluginManager() { }

		#region Events, Delegates, Event Helpers
		public delegate void AssemblyLoadingEventHandler(object sender, AssemblyLoadingEventArgs e);
		public event AssemblyLoadingEventHandler AssemblyLoading;
		private void OnAssemblyLoading(AssemblyLoadingEventArgs e)
		{
			if (this.AssemblyLoading != null)
				this.AssemblyLoading(this, e);
		}

		public delegate void AssemblyLoadedEventHandler(object sender, AssemblyLoadedEventArgs e);
		public event AssemblyLoadedEventHandler AssemblyLoaded;
		private void OnAssemblyLoaded(AssemblyLoadedEventArgs e)
		{
			if (this.AssemblyLoaded != null)
				this.AssemblyLoaded(this, e);
		}

		public delegate void AssemblyFailedLoadingEventHandler(object sender, AssemblyFailedLoadingEventArgs e);
		public event AssemblyFailedLoadingEventHandler AssemblyFailedLoading;
		private void OnAssemblyFailedLoading(AssemblyFailedLoadingEventArgs e)
		{
			if (this.AssemblyFailedLoading != null)
				this.AssemblyFailedLoading(this, e);
		}
		#endregion

		#region Properties
		#region SourceFileExtensionMappings
		private Dictionary<string, SourceFileLanguage> _SourceFileExtensionMappings = new Dictionary<string, SourceFileLanguage>();
		/// <summary>
		/// Contains file extensions (string) mapped to what Language they should be compiled in (SourceFileLanguage).
		/// To populate this List with the defaults, call LoadDefaultFileExtensions()
		/// </summary>
		public Dictionary<string, SourceFileLanguage> SourceFileExtensionMappings
		{
			get { return (_SourceFileExtensionMappings); }
			set { _SourceFileExtensionMappings = value; }
		}
		#endregion

		#region Plugins
		private List<Plugin<ClientInterface>> _Plugins = new List<Plugin<ClientInterface>>();
		/// <summary>
		/// List of Instances of all Loaded Extensions (both compiled and source file)
		/// </summary>
		public List<Plugin<ClientInterface>> Plugins
		{
			get { return (_Plugins); }
			set { _Plugins = value; }
		}
		#endregion

		#region CompiledFileExtensions
		private List<string> _CompiledFileExtensions = new List<string>();
		/// <summary>
		/// List of file extensions to try loading as Compiled Assemblies.
		/// To populate this List with the defaults, call LoadDefaultFileExtensions()
		/// </summary>
		public List<string> CompiledFileExtensions
		{
			get { return (_CompiledFileExtensions); }
			set { _CompiledFileExtensions = value; }
		}
		#endregion

		#region SourceFileReferencedAssemblies
		private List<string> _SourceFileReferencedAssemblies = new List<string>();
		/// <summary>
		/// List of Namespaces that Source Files may reference when being compiled.
		/// By Default this list is empty.
		/// </summary>
		public List<string> SourceFileReferencedAssemblies
		{
			get { return _SourceFileReferencedAssemblies; }
			set { _SourceFileReferencedAssemblies = value; }
		}
		#endregion
		#endregion

		#region Public Methods
		public void UnloadExtension(Plugin<ClientInterface> plugin)
		{
			Plugin<ClientInterface> toRemove = null;

			foreach (Plugin<ClientInterface> plgOn in _Plugins)
			{
				if (plgOn.Filename.ToLower().Trim() == plugin.Filename.ToLower().Trim())
				{
					toRemove = plgOn;
					break;
				}
			}

			_Plugins.Remove(toRemove);
		}

		/// <summary>
		/// Loads the SourceFileExtensionMappings and CompiledFileExtensions Lists with default values.
		/// Default Values Include:
		/// SourceFileExtensionMappings - .cs = CSharp, .vb = Vb, .js = Javascript
		/// CompiledFileExtensions - .dll
		/// </summary>
		public void LoadDefaultFileExtensions()
		{
			_SourceFileExtensionMappings.Add(".cs", SourceFileLanguage.CSharp);
			_SourceFileExtensionMappings.Add(".vb", SourceFileLanguage.VB);
			_SourceFileExtensionMappings.Add(".js", SourceFileLanguage.JavaScript);
			_CompiledFileExtensions.Add(".dll");
		}

		/// <summary>
		/// Searches a given folder for files matching the SourceFileExtensionMappings and CompiledFileExtensions and
		/// attempts to load instances of those files.  Non-Recursive.
		/// </summary>
		/// <param name="folderPath">Full path to load files from</param>
		public void LoadExtensions(string folderPath)
		{
			if (!System.IO.Directory.Exists(folderPath))
				return;

			foreach (string fileOn in System.IO.Directory.GetFiles(folderPath))
			{
				LoadExtension(fileOn);
			}
		}

		/// <summary>
		/// Attempts to load an instance of the given file if it matches SourceFileExtensionMappings or CompiledFileExtensions
		/// </summary>
		/// <param name="filename">Full name of file to load</param>
		public void LoadExtension(string filename)
		{
			//Fire off the loading event, gives consumer a chance to cancel the loading
			AssemblyLoadingEventArgs eargs = new AssemblyLoadingEventArgs(filename);
			OnAssemblyLoading(eargs);

			//If the event consumer cancelled it, no need to continue for this file
			if (eargs.Cancel)
				return;

			//Get the extension of the file
			string extension = (new System.IO.FileInfo(filename)).Extension.TrimStart('.').Trim().ToLower();

			//Check to see if the extension is in the list of source file extensions
			//This allows us to pair up an extension with a particular language it should be compiled in
			//Primative but otherwise how do we know what to compile it as?  We could do some deep analysis of file content,
			// but that is beyond the scope of this library in my opinion
			if (SourceFileExtensionMappings.ContainsKey(extension) || SourceFileExtensionMappings.ContainsKey("." + extension))
			{
				SourceFileLanguage language = SourceFileLanguage.CSharp;

				//Get the matching language
				if (SourceFileExtensionMappings.ContainsKey(extension))
					language = SourceFileExtensionMappings[extension];
				else
					language = SourceFileExtensionMappings["." + extension];

				//Obviously it's a source file, so load it
				this.loadSourceFile(filename, language);
			}
			else if (CompiledFileExtensions.Contains(extension) || CompiledFileExtensions.Contains("." + extension))
			{
				//It's in the compiled file extension list, so just load it
				this.loadCompiledFile(filename);
			}
			else
			{
				//Unknown extension, raise the failed loading event
				AssemblyFailedLoadingEventArgs e = new AssemblyFailedLoadingEventArgs(filename);
				e.PluginType = PluginType.Unknown;
				e.ErrorMessage = "File (" + filename + ") does not match any SourceFileExtensionMappings or CompiledFileExtensions and cannot be loaded.";
				this.OnAssemblyFailedLoading(e);
			}
		}
		#endregion

		#region Private Methods
		private void loadSourceFile(string filename, SourceFileLanguage language)
		{
			bool loaded = false;
			string errorMsg = "";

			//Try compiling the script first
			CompilerResults res = compileScript(filename, _SourceFileReferencedAssemblies, this.getCodeDomLanguage(language));

			//Check for compilation errors
			if (res.Errors.Count <= 0)
			{
				//No errors, then loop through the types in the assembly
				//We don't stop after the first time we find our interface, this way the 
				//Assembly or source file could have multiple types with the desired interface
				//Instead of a 1 interface per file relationship
				foreach (Type t in res.CompiledAssembly.GetTypes())
				{
					//Get the string name of the ClientInterface interface
					string typeName = typeof(ClientInterface).ToString();

					//Try getting the clientinterface from the type
					if (t.GetInterface(typeName, true) != null)
					{
						try
						{
							//Load an instance of this particular Extension and add it to our extensions list
							Plugin<ClientInterface> newExt = new Plugin<ClientInterface>(filename, PluginType.SourceFile, (ClientInterface)res.CompiledAssembly.CreateInstance(t.FullName, true));
							newExt.InstanceAssembly = res.CompiledAssembly;

							_Plugins.Add(newExt);
							loaded = true;
						}
						catch (Exception ex)
						{
							//Some problem in actually creating an instance
							errorMsg = "Error Creating Instance of Compiled Source File (" + filename + "): " + ex.Message;
						}
					}
				}

				//We got through without loading an instance, so we didn't find types with the expected interface
				if (!loaded && String.IsNullOrEmpty(errorMsg))
					errorMsg = "Expected interface (" + typeof(ClientInterface).ToString() + ") was not found in any types in the compiled Source File";
			}
			else
			{
				//Compile time errors
				errorMsg = "Source File Compilation Errors were Detected";
			}

			if (!loaded)
			{
				//Instance was never created, so let's report it and why
				AssemblyFailedLoadingEventArgs e = new AssemblyFailedLoadingEventArgs(filename);
				e.PluginType = PluginType.SourceFile;
				e.SourceFileCompilerErrors = res.Errors;
				e.ErrorMessage = errorMsg;
				this.OnAssemblyFailedLoading(e);
			}
		}

		private void loadCompiledFile(string filename)
		{
			bool loaded = false;
			string errorMsg = "";
			Assembly compiledAssembly = null;

			//Load the assembly to memory so we don't lock up the file
			byte[] assemblyFileData = System.IO.File.ReadAllBytes(filename);

			//Load the assembly
			try { compiledAssembly = Assembly.Load(assemblyFileData); }
			catch { errorMsg = "Compiled Assembly (" + filename + ") is not a valid Assembly File to be Loaded."; }

			if (compiledAssembly != null)
			{
				//Go through the types we need to find our clientinterface
				foreach (Type t in compiledAssembly.GetTypes())
				{
					//Just the string name of our ClientInterface since it's unknown at compile time
					string typeName = typeof(ClientInterface).ToString();

					//Try getting the interface from the current type
					if (t.GetInterface(typeName, true) != null)
					{
						try
						{
							//Load an instance of this particular Extension and add it to our extensions list
							Plugin<ClientInterface> newExt = new Plugin<ClientInterface>(filename, PluginType.Compiled, (ClientInterface)compiledAssembly.CreateInstance(t.FullName, true));
							newExt.InstanceAssembly = compiledAssembly;

							_Plugins.Add(newExt);
							loaded = true;
						}
						catch (Exception ex)
						{
							//Creating an instance failed for some reason, pass along that exception message
							errorMsg = "Error Creating Instance of Compiled Assembly (" + filename + "): " + ex.Message;
						}
					}
				}

				//If no instances were loaded at this point, means we never found types with the ClientInterface
                if (!loaded && String.IsNullOrEmpty(errorMsg))
                {
                    errorMsg = "Expected interface (" + typeof(ClientInterface).ToString() + ") was not found in Compiled Assembly (" + filename + ")";
                }
			}

            if (!loaded)
            {
                //Nothing was loaded, report it
                var e = new AssemblyFailedLoadingEventArgs(filename);
                e.PluginType = PluginType.Compiled;
                e.ErrorMessage = errorMsg;
                this.OnAssemblyFailedLoading(e);
            }
            else
            {
                this.OnAssemblyLoaded( new AssemblyLoadedEventArgs(filename));
            }
		}

		private CompilerResults compileScript(string filename, List<string> references, string language)
		{
			System.CodeDom.Compiler.CodeDomProvider cdp = System.CodeDom.Compiler.CodeDomProvider.CreateProvider(language);

			// Configure parameters
			CompilerParameters parms = new CompilerParameters();
			parms.GenerateExecutable = false; //Don't make exe file
			parms.GenerateInMemory = true; //Don't make ANY file, do it in memory
			parms.IncludeDebugInformation = false; //Don't include debug symbols

			//Add references passed in 
            if (references != null)
            {
                parms.ReferencedAssemblies.AddRange(references.ToArray());
            }

			// Compile			
			CompilerResults results = cdp.CompileAssemblyFromFile(parms, filename);

			return results;
		}

		private string getCodeDomLanguage(SourceFileLanguage language)
		{
			string result = "C#";

			switch (language)
			{
				case SourceFileLanguage.CSharp: result = "C#"; break;
				case SourceFileLanguage.VB: result = "VB"; break;
				case SourceFileLanguage.JavaScript: result = "JS"; break;
			}

			return result;
		}
		#endregion
	}
}
